/**
 * 测试用例编组
 *
 * Created by nongzhizhong on 2022/02/15.
 */
const mongoose = require('mongoose');
const functions = require('./functions');
const tests = require('./tests');
const numbers = require('./numbers');
const projectVersion = require('./project-versions');
const TestCaseGroup = require('./mongo/test-case-groups');
const utility = require('../models/utility');
const TestCase = require('./mongo/test-case');
const BlackBoxCovered = require('./mongo/black-box-covered');
const { eventHub, eventKind } = require('./event-hub');

// 编组children元素子类型
const childTypes = {
  testCaseGroup: 'group',
  testCase: 'testcase',
};

function getJson(group) {
  return {
    groupId: group._id,
    number: group.number,
    type: childTypes.testCaseGroup,
    description: group.description,
    result: group.result || {},
    path: group.path,
    children: group.children,

  };
}

async function addGroup({
  projectId,
  versionId,
  versionType,
  fileId,
  functionId,
  locale,
  parentId = undefined,
  description = undefined,
  addDefaultTestCase = true,
}) {
  let groupPath = [];
  const resGroup = {
    groupId: '',
    number: 0,
    type: childTypes.testCaseGroup,
    description,
    result: {
      kind: tests.resultKind.unknown,
    },
    children: [],
  };
  const doc = {
    projectId,
    versionId,
    fileId,
    description,
    functionId,
    children: [],
  };
  const { number } = await numbers.next(numbers.getTestcaseGroupNumberName(functionId));
  doc.number = number;
  resGroup.number = number;
  const group = await (new TestCaseGroup(doc)).save();
  const groupId = group._id;
  resGroup.groupId = groupId;
  if (addDefaultTestCase) {
    let funcVariables = await functions.getFunctionVariables(functionId);
    if (versionType === projectVersion.versionType.integrationTest) {
      funcVariables = await functions.functionVariableSpliceFileId(funcVariables,
        { spliceFileId: true, entryFuncFileId: fileId.toString() });
      const sourceFileExts = [];
      // 获取源码文件类型
      const { languages } = await projectVersion.getVersion(versionId);
      Object.keys(languages).forEach((key) => {
        if (languages[key].suffixes) {
          sourceFileExts.push(...languages[key].suffixes);
        }
      });
      // 处理集成测试绝对地址目标
      await functions.IntegrationAddInvokeFuncFixedAddressBases({ ...funcVariables, versionId }, sourceFileExts, versionType);
    }
    let testCase = {
      data: {},
      description: '',
    };
    tests.addDefaultValue(testCase, funcVariables);
    const { data } = tests.getJson(testCase, locale);
    testCase = (await tests.addTest(
      projectId,
      versionId,
      fileId,
      functionId,
      { data, description: '' }
    )).toObject({ minimize: false });
    testCase = tests.getSummaryJson(testCase, locale);
    testCase.type = childTypes.testCase;
    doc.children.push({
      id: testCase.testId,
      childType: childTypes.testCase,
    });
    resGroup.children.push(testCase);
  }
  if (parentId) {
    parentId = mongoose.Types.ObjectId(parentId);
    const { path: parentPath } = await TestCaseGroup.findById(parentId);
    groupPath = parentPath;
    await TestCaseGroup.updateOne({ _id: parentId }, {
      $push: { children: { id: groupId, childType: childTypes.testCaseGroup } },
    });
  }
  groupPath.push(groupId);
  await TestCaseGroup.updateOne(
    { _id: groupId }, { $set: { path: groupPath, children: doc.children } }
  );
  // eslint-disable-next-line no-use-before-define
  await removeTestResultByTopGroupId(groupPath[0], functionId);
  return resGroup;
}

async function getGroupById(groupIds) {
  if (utility.isArrayType(groupIds)) {
    groupIds = groupIds.map((groupId) => mongoose.Types.ObjectId(groupId));
  } else {
    groupIds = [].concat(mongoose.Types.ObjectId(groupIds));
  }
  return TestCaseGroup.find({ _id: { $in: groupIds } });
}

async function getAllTestsByGroupId(groupId, functionId = null) {
  if (!functionId) {
    const groups = await getGroupById(groupId);
    if (groups.length === 0) {
      return [];
    }
    functionId = groups[0].functionId;
  } else {
    functionId = mongoose.Types.ObjectId(functionId);
  }
  const groupMap = new Map();
  const testCaseGroups = await TestCaseGroup.find(
    { functionId, path: mongoose.Types.ObjectId(groupId) }
  ).sort({ _id: 1 });
  const testCaseIds = [];
  if (testCaseGroups.length === 0) {
    return testCaseIds;
  }
  let topGroup = null;
  testCaseGroups.forEach((group) => {
    if (!topGroup) {
      topGroup = group;
    }
    groupMap.set(group._id.toString(), group);
  });
  function getTestIdByOrder(group) {
    if (group.children) {
      group.children.forEach((element) => {
        const elementId = element.id.toString();
        if (element.childType === childTypes.testCase) {
          testCaseIds.push(elementId);
        } else if (groupMap.has(elementId)) {
          getTestIdByOrder(groupMap.get(elementId));
        }
      });
    }
  }
  getTestIdByOrder(topGroup);
  return testCaseIds;
}

async function removeGroupCoverage(groupId, functionId) {
  return functions.removeCfgCoverageWithTestId(functionId, groupId);
}

/**
 * 如果测试用例分组只有这一个测试用例，则该分组也被删除，然后向上递归执行直到碰到不为空的分组（或者到达顶层）
 */
async function clearEmptyTestGroups(testCaseOrGroupId, type, functionId) {
  const group = await TestCaseGroup.findOne({
    functionId: mongoose.Types.ObjectId(functionId),
    children: { $elemMatch: { id: mongoose.Types.ObjectId(testCaseOrGroupId), childType: type } },
  });
  if (!group) {
    // 该用例（或分组）不在任何分组之下，说明到达顶层
    return {
      groupId: null,
      testCaseIds: [],
    };
  }
  if (group.children.length > 1) {
    const children = group.children.filter(
      (x) => !(testCaseOrGroupId.toString() === x.id.toString() && x.childType === type),
    );
    await TestCaseGroup.updateOne(
      { _id: group._id },
      { $set: { children } },
    );
    const affectedTestCases = await getAllTestsByGroupId(group._id, functionId);
    return {
      groupId: group._id,
      testCaseIds: affectedTestCases,
    };
  }
  await TestCaseGroup.deleteOne({ _id: group._id });
  if (group.path.length === 1) {
    await removeGroupCoverage(group._id, group.functionId);
  }
  return clearEmptyTestGroups(group._id, 'group', functionId);
}

async function removeTestResultByTopGroupId(groupId, functionId) {
  const testcases = await getAllTestsByGroupId(groupId);
  await Promise.all(testcases.map((testId) => tests.removeResultOfExecution(testId)));
  return removeGroupCoverage(groupId, functionId);
}

async function removeTestResultByTestId(testId, functionId, onlySelf = false) {
  if (!functionId) {
    return Promise.resolve();
  }
  const group = await TestCaseGroup.findOne({
    functionId: mongoose.Types.ObjectId(functionId),
    children: { $elemMatch: { id: mongoose.Types.ObjectId(testId) } },
  });
  if (!group) {
    return tests.removeResultOfExecution(testId);
  }
  if (onlySelf) {
    return Promise.all([
      tests.removeResultOfExecution(testId),
      removeGroupCoverage(group.path[0], functionId),
    ]);
  }
  return removeTestResultByTopGroupId(group.path[0], functionId);
}

async function editTestCaseGroup(body, groupId) {
  const doc = {};
  Object.keys(body).forEach((key) => {
    doc[key] = body[key];
  });
  const group = await TestCaseGroup.findOneAndUpdate(
    { _id: groupId },
    { $set: doc },
    { new: true }
  );
  if (doc.children) {
    await removeTestResultByTopGroupId(group.path[0], group.functionId);
    await functions.updateFunctionCoverage(group.functionId);
    const func = await functions.getFunction(group.functionId);
    if (func.versionType === projectVersion.versionType.integrationTest) {
      await functions.updateFunctionInvokeCoverage(group.functionId);
    }
  }
}

function completionTestCaseOfGroup(group, testCases) {
  const newChildren = [];
  const parentId = group.groupId;
  group.children.forEach((item) => {
    if (item.childType === childTypes.testCaseGroup) {
      newChildren.push({ id: item.id, type: childTypes.testCaseGroup, parentId });
    }
    testCases.forEach((testCase) => {
      if (testCase.testId.toString() === item.id.toString()) {
        newChildren.push({ ...testCase, parentId });
      }
    });
  });
  group.children = newChildren;
  if (group.path.length > 1) {
    [group.parentId] = group.path.slice(-2);
  }
  return group;
}

async function getGroupByFunctionId(functionId) {
  return TestCaseGroup.find({ functionId: mongoose.Types.ObjectId(functionId) });
}

async function getGroupByFunctionIds(functionIds) {
  const fixedFunctionIds = functionIds.map((n) => mongoose.Types.ObjectId(n));
  return TestCaseGroup.find({ functionId: { $in: fixedFunctionIds } });
}

async function getGroupByFileId(fileId) {
  return TestCaseGroup.find({ fileId: mongoose.Types.ObjectId(fileId) });
}

async function getTopGroupsByTestIds(testIds, functionId) {
  const groupSet = new Set();
  const testIdsSet = new Set();
  testIds.forEach((testId) => testIdsSet.add(testId.toString()));
  const groups = await getGroupByFunctionId(functionId);
  if (!groups.length) {
    return [];
  }
  groups.forEach((group) => {
    if (!group.children) {
      return;
    }
    group.children.forEach((element) => {
      if (testIdsSet.has(element.id.toString())) {
        groupSet.add(group.path[0].toString());
      }
    });
  });
  return [...groupSet];
}
function addChildGroup(group, groups) {
  const newChildren = [];
  group.children.forEach((child) => {
    if (child.type === childTypes.testCaseGroup) {
      groups.forEach((item) => {
        const groupId = child.id || child.groupId;
        if (item.groupId.toString() === groupId.toString()) {
          newChildren.push(item);
          item.children.forEach((itemChild) => {
            if (itemChild.type === childTypes.testCaseGroup) {
              addChildGroup(item, groups);
            }
          });
        }
      });
    } else {
      newChildren.push(child);
    }
  });
  group.children = newChildren;
}

function getResultKind(group, testCaseResKinds) {
  if (!group.result) {
    group.result = {};
  }
  let childKind;
  for (let i = 0; i < group.children.length; i += 1) {
    const item = group.children[i];
    if (item.type === childTypes.testCaseGroup) {
      // 获取子编组结果
      childKind = getResultKind(item, []);
      testCaseResKinds.push(childKind);
    } else {
      testCaseResKinds.push(item.result.kind);
    }
  }
  // 编组结果处理:通过<通过*/无结果<未通过<未执行<运行时错误/执行异常
  let resKind;
  if (testCaseResKinds.includes(tests.resultKind.runtimeError)) {
    resKind = tests.resultKind.runtimeError;
  } else if (testCaseResKinds.includes(tests.resultKind.timeout)) {
    resKind = tests.resultKind.timeout;
  } else if (testCaseResKinds.includes(tests.resultKind.systemError)) {
    resKind = tests.resultKind.systemError;
  } else if (testCaseResKinds.includes(tests.resultKind.instrumentError)) {
    resKind = tests.resultKind.instrumentError;
  } else if (testCaseResKinds.includes(tests.resultKind.compileError)) {
    resKind = tests.resultKind.compileError;
  } else if (testCaseResKinds.includes(tests.resultKind.unknown)) {
    resKind = tests.resultKind.unknown;
  } else if (testCaseResKinds.includes(tests.resultKind.failed)) {
    resKind = tests.resultKind.failed;
  } else if (testCaseResKinds.includes(tests.resultKind.nonStrictSucceed)) {
    resKind = tests.resultKind.nonStrictSucceed;
  } else if (testCaseResKinds.includes(tests.resultKind.noResult)) {
    resKind = tests.resultKind.noResult;
  } else {
    resKind = tests.resultKind.succeed;
  }
  group.result.kind = resKind;
  return resKind;
}

function getRelatedGroups(currentGroup, groups) {
  const relationGroups = [];
  groups.forEach((group) => {
    const groupIds = group.path;
    if ((groupIds.map((id) => id.toString())).includes(currentGroup.groupId.toString())) {
      relationGroups.push(group);
    }
  });
  return relationGroups;
}

function buildTreeRecur(groups, groupMap, testCaseMap) {
  return groups.map(({ _id: groupId, children }) => {
    const tmp = children.map(({ id: childId, childType }) => {
      if (childType === 'group') {
        return buildTreeRecur([groupMap[childId.toString()]], groupMap, testCaseMap);
      }
      if (childType === 'testcase') {
        const { _id, ...attrs } = testCaseMap[childId.toString()];
        return [{ testcase: _id, ...attrs }];
      }
      return `invalid type ${childType}`;
    });
    return { testcase: groupId, testcaselist: Array.prototype.concat(...tmp) };
  });
}

function buildIdMap(objList) {
  return Object.fromEntries(objList.map((elem) => [elem._id.toString(), elem]));
}

/**
 * 构造执行用例（或生成目标机脚本）的过程中，核心层需要的，带有编组的测试用例树。
 *
 * @param {Array<TestCaseGroup>} groups: [{_id, path, children, ...}, ...]
 * @param {Array<TestCase>} testCases: [{_id, ...}, ...]
 * @returns {...}
 *
 * sample:
 *
 * const sampleGroups = [
 *   { _id: 'g1', path: ['g1'],
 *    children: [{ id: 'g2', childType: 'group' }, { id: 'g3', childType: 'group' },
 *               { id: 'c1', childType: 'testcase' }] },
 *
 *   { _id: 'g2', path: ['g1', 'g2'],
 *    children: [{ id: 'g4', childType: 'group' }, { id: 'c2', childType: 'testcase' }] },
 *
 *   { _id: 'g3', path: ['g1', 'g3'],
 *    children: [{ id: 'g5', childType: 'group' }, { id: 'c3', childType: 'testcase' }] },
 *
 *   { _id: 'g4', path: ['g1', 'g2', 'g4'],
 *    children: [{ id: 'g6', childType: 'group' }, { id: 'c4', childType: 'testcase' }] },
 *
 *   { _id: 'g5', path: ['g1', 'g3', 'g5'],
 *    children: [{ id: 'c5', childType: 'testcase' }, { id: 'c7', childType: 'testcase' }] },
 *
 *   { _id: 'g6', path: ['g1', 'g2', 'g4', 'g6'],
 *    children: [{ id: 'c8', childType: 'testcase' }] },
 *
 * ]  ;
 * const sampleTestCases = [
 *   { _id: 'c1', data: 'blah1' },
 *   { _id: 'c2', data: 'blah2' },
 *   { _id: 'c3', data: 'blah3' },
 *   { _id: 'c4', data: 'blah4' },
 *   { _id: 'c5', data: 'blah5' },
 *   { _id: 'c6', data: 'blah6' },
 *   { _id: 'c7', data: 'blah7' },
 *   { _id: 'c8', data: 'blah8' },
 *   { _id: 'c9', data: 'blah9' },
 * ];
 *
 * then calling buildTreeWithGroupsAndTestCases(sampleGroups, sampleTestCases) will return:
 *
 * [
 *   { testcase: 'c6', data: 'blah6' },
 *   { testcase: 'c9', data: 'blah9' },
 *   {
 *     testcase: 'g1',
 *     testcaselist: [
 *       {
 *         testcase: 'g2',
 *         testcaselist: [
 *           {
 *             testcase: 'g4',
 *             testcaselist: [
 *               {
 *                 testcase: 'g6',
 *                 testcaselist: [ { testcase: 'c8', data: 'blah8' } ]
 *               },
 *               { testcase: 'c4', data: 'blah4' }
 *             ]
 *           },
 *           { testcase: 'c2', data: 'blah2' }
 *         ]
 *       },
 *       {
 *         testcase: 'g3',
 *         testcaselist: [
 *           {
 *             testcase: 'g5',
 *             testcaselist: [
 *               { testcase: 'c5', data: 'blah5' },
 *               { testcase: 'c7', data: 'blah7' }
 *             ]
 *           },
 *           { testcase: 'c3', data: 'blah3' }
 *         ]
 *       },
 *       { testcase: 'c1', data: 'blah1' }
 *     ]
 *   }
 * ]
 *
 */
function buildTreeWithGroupsAndTestCases(groups, testCases, groupIds = undefined) {
  let topLevelGroups = groups.filter(
    ({ path, _id }) => path.length === 1 && path[0].toString() === _id.toString()
  );

  const topLevelTestCases = testCases.filter(
    ({ _id: testCaseId }) => !groups.find(
      ({ children }) => children.find(({ id }) => id.toString() === testCaseId.toString())
    )
  ).map(({ _id, ...attrs }) => ({ testcase: _id, ...attrs }));

  if (groupIds) {
    // partial-testcases run
    topLevelGroups = topLevelGroups.filter(({ _id }) => groupIds.includes(_id.toString()));
  }
  return [
    ...topLevelTestCases,
    ...buildTreeRecur(topLevelGroups, buildIdMap(groups), buildIdMap(testCases)),
  ];
}

async function buildTreeWithFunctionIdAndTestCases(functionId, testCases, groupIds) {
  const groups = await getGroupByFunctionId(functionId);
  return buildTreeWithGroupsAndTestCases(groups, testCases, groupIds);
}

async function getTree(groupId, func, locale, resultKinds, kinds) {
  const functionId = func._id;
  let groups = await getGroupByFunctionId(functionId);
  const testCases = (await tests.getTestsOfFunctions(functionId)).map((testCase) => ({
    type: childTypes.testCase,
    ...tests.getSummaryJson(testCase, locale),
    identifier: tests.getTestIdentifier({
      ...testCase,
      functionName: func.functionName,
      filepath: func.filepath,
      testIdentifierRule: func.testIdentifierRule,
    }),
  }));
  const includeTestCases = [];
  const excludeTestCases = [];
  let tree = {
    groupId: '',
    number: null,
    type: 'group',
    description: '',
    result: {},
    children: [],
  };
  // 区分开编组中测试用例和非编组中测试用例
  testCases.forEach((testCase) => {
    let exist = false;
    groups.forEach((group) => {
      group.children.forEach((item) => {
        if (item.childType === childTypes.testCase
          && item.id.toString() === testCase.testId.toString()) {
          exist = true;
        }
      });
    });
    if (exist) {
      includeTestCases.push(testCase);
    } else {
      excludeTestCases.push({ type: childTypes.testCase, ...testCase });
    }
  });
  // 补全编组中测试用例的信息
  groups = groups.map((group) => completionTestCaseOfGroup(getJson(group), includeTestCases));
  if (groupId) {
    groups.forEach((group) => {
      if (group.groupId.toString() === groupId.toString()) {
        tree = group;
      }
    });
    addChildGroup(tree, getRelatedGroups(tree, groups));
    getResultKind(tree, []);
  } else {
    tree.children = groups.filter((group) => {
      if (group.path.length === 1) { // 顶层编组
        addChildGroup(group, getRelatedGroups(group, groups));
        return true;
      }
      return false;
    });
    tree.children = tree.children.concat(excludeTestCases);
    getResultKind(tree, []);
    // 结果筛选
    if (resultKinds) {
      resultKinds = [].concat(resultKinds).map((i) => parseInt(i, 10));
      tree.children = tree.children.filter((item) => resultKinds.includes(item.result.kind));
    }
    // 用例kind筛选
    if (kinds) {
      kinds = [].concat(kinds).map((i) => parseInt(i, 10));
      const options = tests.getTestOptions({ kind: kinds });
      options.functionId = mongoose.Types.ObjectId(functionId);
      options._id = { $in: excludeTestCases.map((testCase) => testCase.testId) };
      const testCaseIds = (await TestCase.find(options)).map((testCase) => testCase._id.toString());
      tree.children = tree.children.filter((item) => {
        if (item.type === childTypes.testCaseGroup && kinds.includes(tests.testKind.modified)) {
          return true;
        }
        if (item.type === childTypes.testCase && testCaseIds.includes(item.testId.toString())) {
          return true;
        }
        return false;
      });
    }
    // 顶层编组和用例混合排序
    tree.children = tree.children.sort((c1, c2) => {
      const c1Time = c1.groupId ? c1.groupId.getTimestamp().getTime()
        : c1.testId.getTimestamp().getTime();
      const c2Time = c2.groupId ? c2.groupId.getTimestamp().getTime()
        : c2.testId.getTimestamp().getTime();
      if (c1.type === c2.type) {
        return c1.number > c2.number ? 1 : -1;
      }
      return c1Time > c2Time ? 1 : -1;
    });
  }
  // 如果存在黑盒测试结果，将在第一条展示一个虚拟用例
  const blackBoxCovered = await BlackBoxCovered.findOne({ functionId: mongoose.Types.ObjectId(functionId) });
  // 只有顶层展示虚拟用例
  if (!groupId && blackBoxCovered) {
    tree.children.unshift({
      type: 'coverage',
      testId: blackBoxCovered._id,
    });
  }
  return tree;
}

async function reshuffleGroupNumber(functionId) {
  const numberName = numbers.getTestcaseGroupNumberName(functionId);
  functionId = mongoose.Types.ObjectId(functionId);
  const group = await TestCaseGroup.findOne(
    { functionId },
    { number: 1 },
    { sort: { number: -1 } }
  );
  await numbers.restore(numberName, group ? group.number : 0);
}
/**
 * 获取测试用例所属编组信息
 * @param {String} testCaseId 测试用例id
 * @param {Object} fields 需要返回的字段信息
 * return 测试用例所属编组（只获取一层编组）
 */
async function getGroupByTestCaseId(testCaseId, fields = { _id: 1, number: 1, children: 1 }) {
  const group = await TestCaseGroup.findOne({
    children: {
      $elemMatch: {
        id: mongoose.Types.ObjectId(testCaseId),
        childType: 'testcase',

      },
    },
  }, fields);
  return group;
}
eventHub.on([eventKind.EVENT_FUNCTION_REMOVE, eventKind.EVENT_FUNCTION_RESET], ({ functionIds, versionId }) => {
  const filter = { versionId: mongoose.Types.ObjectId(versionId) };
  if (!functionIds) {
    return TestCaseGroup.deleteMany(filter);
  }
  return utility.arrayChunkOperation(utility.arrayChunk(functionIds, 500), 1, (funcIds) => {
    filter.functionId = { $in: funcIds.map((id) => mongoose.Types.ObjectId(id)) };
    return TestCaseGroup.deleteMany(filter);
  });
});

module.exports = {
  type: childTypes,

  addGroup,

  buildTreeWithFunctionIdAndTestCases,

  completionTestCaseOfGroup,
  clearEmptyTestGroups,

  editTestCaseGroup,

  getGroupById,
  getAllTestsByGroupId,
  getGroupByFileId,
  getGroupByFunctionId,
  getGroupByFunctionIds,
  getTree,
  getGroupByTestCaseId,
  getTopGroupsByTestIds,

  removeGroupCoverage,
  removeTestResultByTestId,
  removeTestResultByTopGroupId,
  reshuffleGroupNumber,

};
